/*

   **********************************************************************
   *                                                                    *
   * This file contains the code for an image object for a content-     *
   * based image retrieval application that "summarizes" an image and   *
   * sends the resulting image "signature" to a server for              *
   * classification. The server returns the classification result in    *
   * the form of a Web page containing example images of the input      *
   * image's category.                                                  *
   *                                                                    *
   * The image class defined here is rather specific to this particular *
   * application and is not really designed for broad reuse (just yet). *
   *                                                                    *
   **********************************************************************
*/

/*
   **********************************************************************
   *                                                                    *
   * Gist   -- content-based image retrieval application for the gPhone *
   *                                                                    *
   * Copyright (C) 2011 Yong Zeng                                       *
   *                                                                    *
   * This file is part of Gist.                                       *
   *                                                                    *
   * Gist is free software; you can redistribute it and/or modify it  *
   * under the terms of the GNU General Public License as published by  *
   * the Free Software Foundation; either version 2 of the License, or  *
   * (at your option) any later version.                                *
   *                                                                    *
   * Gist is distributed in the hope that it will be useful, but      *
   * WITHOUT ANY WARRANTY; without even the implied warranty of         *
   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU   *
   * General Public License for more details.                           *
   *                                                                    *
   * You should have received a copy of the GNU General Public License  *
   * along with Goggle; if not, write to the Free Software Foundation,  *
   * Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.       *
   *                                                                    *
   **********************************************************************
*/

//----------------------- PACKAGE SPECIFICATION -------------------------

package vision.gistcomputing ;

//------------------------------ IMPORTS --------------------------------

// Android UI support
import android.widget.TextView ;

// Android graphics support
import android.graphics.Bitmap ;

// Android application and OS support
import android.content.Context ;
import android.app.Activity ;
import android.content.Intent ;
import android.os.Handler ;
import android.os.Message ;
import android.os.Bundle ;

// Android utilities
import android.net.Uri ;
import android.util.Log ;

// Java I/O support
import java.io.FileInputStream;
import java.io.InputStream ;
import java.io.FileNotFoundException ;
import java.io.IOException ;

// Java thread support
import java.lang.Thread ;

// Java utilities
import java.text.DecimalFormat ;

import vision.gistcomputing.R;

//----------------- THE CLASSIFICATION RESULTS SCREEN -------------------

/**
   This class implements the computations screen of a content-based image
   retrieval application. This activity performs the gist vector
   computations for the input image in a worker thread and displays the
   progress in the main thread's UI. The classification results returned
   by the server are displayed in a Web browser.
*/
public class GistActivity extends Activity {

/// The main screen and the new image capture screen both pass the ID of
/// the selected or newly created thumbnail to this activity (which then
/// proceeds to compute the gist vector of this image). This thumbnail ID
/// parameter is passed between the activities using Android's intent
/// extras mechanism. To be able to properly store and retrieve this
/// value, all the concerned activities must agree on a suitable key/tag
/// to use. This string is that key.
public static final String EXTRAS_THUMBNAIL_ID = "extras_thumbnail_id" ;

/// Since computing gist vectors can be quite CPU-intensive, the gist
/// activity spawns a worker thread to do all the number crunching.
/// However, to let users know what is going on, the main thread expects
/// the worker thread to send it periodic status or other messages. These
/// codes serve as the IDs of the messages exchanged between the main and
/// worker threads.
///@{
private static final int STATUS_UPDATE_MSG    = 1 ;
private static final int RESULT_AVAILABLE_MSG = 2 ;
private static final int GIST_FAILED_MSG      = 3 ;
///@}

/// This handler object is the one used by the gist computation worker
/// thread to send various notifications to the main thread.
private Handler m_status_handler = new Handler() {
   public void handleMessage(Message M) {
      switch (M.what) {
         case STATUS_UPDATE_MSG:
            display_status_message(M.arg1) ;
            break ;

         case RESULT_AVAILABLE_MSG:
            display_classification_result((String) M.obj) ;
            break ;

         case GIST_FAILED_MSG:
            display_failure_msg(M.arg1) ;
            break ;
      }
   }
} ;

/// Status notifications sent from the gist computation worker thread to
/// the main thread are displayed in this UI element.
private TextView m_status_msg ;

/// Just a means of keeping track of how long it takes to perform the
/// gist computations.
private long m_start_time ;

//-------------------------- INITIALIZATION -----------------------------

/// A convenience function for starting this activity in order to compute
/// the gist vector for the selected or newly captured image and send it
/// to the server for classification.
public static final void start(Context C, long thumbnail_id)
{
   Intent I = new Intent(C, GistActivity.class) ;
   I.putExtra(EXTRAS_THUMBNAIL_ID, thumbnail_id) ;
   C.startActivity(I) ;
}

/// This method is called when the activity is first created. In the case
/// of the Goggle application, this activity would be started by either
/// the main screen or the camera preview-and-capture screen. Both these
/// "parent" activities must pass the ID of the selected (or newly
/// created) image's thumbnail to the gist activity using Android's
/// intent extras mechanism.
@Override protected void onCreate(Bundle saved_state)
{
   super.onCreate(saved_state) ;
   setContentView(R.layout.gist) ;
   m_status_msg = (TextView) findViewById(R.id.gist_status_msg) ;

   // From the extras bundle, retrieve the thumbnail ID of the selected
   // (or newly created) image. Then use this thumbnail ID to locate the
   // full-sized version of the input image.
   int input_image_id = Utils.full_picture_id(this,
      getIntent().getExtras().getLong(EXTRAS_THUMBNAIL_ID)) ;

   // Spawn worker thread to perform gist computations on input image
   new GistThread(this, m_status_handler, input_image_id) ;
}

//-------------------------- STATUS MESSAGES ----------------------------

/// This method displays the specified status message. Additionally,
/// based on the status messages sent by the gist computation worker
/// thread, it records and reports the amount of time spent on the gist
/// vector computations. This time does not include the amount of time
/// taken to load the image from the database or to send the gist vector
/// to the server and receive a reply.
private void display_status_message(int msg_id)
{
   switch (msg_id)
   {
      case R.string.computing_gist:
         m_start_time = System.currentTimeMillis() ;
         break ;

      case R.string.sending_gist_to_server:
         Utils.notify_long(this, gist_time_msg()) ;
         break ;
   }
   m_status_msg.setText(getString(msg_id)) ;
}

// A quick helper to return the number of seconds it took to compute the
// gist vector. The result is returned as a string suitable for display
// on the application's UI.
private String gist_time_msg()
{
   float gist_time = (System.currentTimeMillis() - m_start_time)/1000.0f ;
   DecimalFormat format = new DecimalFormat("#.#") ;
   return format.format(gist_time) + " " + getString(R.string.gist_time_msg) ;
}

//----------------------------- CLEAN-UP --------------------------------

/// This method starts a Web browser on the classification results URL
/// sent by the Goggle server and winds up this (computational) activity.
private void display_classification_result(String url)
{
   WebViewActivity.start(this, url) ;
   finish();
}

/// This method informs the user that the gist vector for the supplied
/// input image could not be computed and then returns to the main
/// activity.
private void display_failure_msg(int msg_id)
{
   Utils.notify_long(this, getString(msg_id)) ;
   finish() ;
}

//------------------ GIST VECTOR COMPUTATION THREAD ---------------------

/**
   Since computing gist vectors is a fairly expensive operation, the gist
   activity's main thread spawns this worker thread to perform the
   necessary number crunching. This allows the activity's main thread to
   continue to respond to system events, thereby avoiding Android's
   dreaded "Application not responding" messages.
*/
private class GistThread extends Thread {

/// To load the image from the Android MediaStore.Images database and for
/// other such tasks, the worker thread will need a valid Android
/// context.
private Context m_context ;

/// The gist activity's worker thread for computing gist vectors sends
/// messages to the activity's main thread at the start or end of each
/// milestone so that the main thread can update the UI to let users know
/// what's going on. These messages are relayed to the main thread via a
/// suitable Handler object passed to the worker thread by the gist
/// activity.
private Handler m_status_handler ;

/// The gist worker thread needs to know the ID of the image on which it
/// is supposed to operate. This must be specified by the gist activity.
private int m_image_id ;

/// When the worker thread object is created, we immediately start it
/// running so that the main activity (this class's client) only needs to
/// instantitate it and not have to go through any extensive set-up
/// procedures.
public GistThread(Context C, Handler H, int image_id)
{
   super("GistThread") ;

   m_context = C ;
   m_status_handler = H ;
   m_image_id = image_id ;

   start() ;
}

/// This is the "main" method for the gist vector worker thread. It goes
/// through each of the required steps for computing the gist vector
/// associated with the supplied image.
public void run()
{
   try
   {
      String classification_result_url ;
      if (Settings.send_entire_image())
      {
         InputStream is = Utils.load_image(m_context, m_image_id) ;
         classification_result_url = send_to_server(is) ;
         is.close() ;
      }
      else // compute and send gist vector to server
      {
         float[] gist_vector = compute_gist_vector(load_and_rescale_image()) ;
         Utils.save(gist_vector,
                    "/data/data/cs546.group7/files/gist_vector.txt") ;
         classification_result_url = send_to_server(gist_vector) ;
      }

      // That's it: we're done; time to display classification result...
      Utils.notify_main_thread(m_status_handler, RESULT_AVAILABLE_MSG,
                               classification_result_url) ;
   }
   catch (FileNotFoundException e)
   {
      Log.e("Goggle", "unable to read image ID " + m_image_id, e) ;
      Utils.notify_main_thread(m_status_handler, GIST_FAILED_MSG,
                               R.string.image_exist_error) ;
   }
   catch (IOException e)
   {
      Log.e("Goggle", "unable to close bitmap input stream", e) ;
      Utils.notify_main_thread(m_status_handler, GIST_FAILED_MSG,
                               R.string.image_read_error) ;
   }
   catch (GistEstimator.UnknownEstimator e)
   {
      Log.e("Goggle", "unknown GistEstimator", e) ;
      Utils.notify_main_thread(m_status_handler, GIST_FAILED_MSG,
                               R.string.unknown_gist_estimator) ;
   }
   catch (RuntimeException e)
   {
      Log.e("Goggle", "unable to compute gist vector", e) ;
      Utils.notify_main_thread(m_status_handler, GIST_FAILED_MSG,
                               R.string.gist_failed_msg) ;
   }
}

// This method loads the input image from the Android database, rescaling
// it to 160x120 pixels and replacing color information with luminance
// values.
private Image
load_and_rescale_image() throws FileNotFoundException, IOException
{
   Utils.notify_main_thread(m_status_handler, STATUS_UPDATE_MSG,
                            R.string.loading_image) ;

   // To load a bitmap of a specific size from an input stream, we need
   // to rewind the input stream because Utils.load_bitmap() first
   // determines the size of the bitmap to be loaded and then loads the
   // bitmap, applying appropriate scaling factors. The act of
   // determining the bitmap's size from the input stream will result in
   // the stream being read to its end. Thus, to load the bitmap from it
   // in the next step, we will have to rewind the stream. The only way
   // to do that is to close the stream and reopen it. The following
   // function object does just that.
   final Context  context  = m_context ;
   final int      image_id = m_image_id ;
   Utils.Function rewinder = new Utils.Function() {
         public Object apply(Object O) {
            try
            {
               InputStream is = (InputStream) O ;
               is.close() ;
               is = Utils.load_image(context, image_id) ;
               return is ;
            }
            catch (IOException e)
            {
               Log.e("Goggle",
                     "hell broke loose trying to load image " + image_id, e) ;
               return null ;
            }
         }} ;

   // Load a 160x120 bitmap from the input stream for the image ID
   InputStream is = Utils.load_image(context, image_id) ;
   Bitmap B = Utils.load_bitmap(160, 120,
      new Utils.InputStreamBitmapDecoder(is, rewinder)) ;
   is.close() ;

   // Retrieve the bitmap's pixels
   int[] color_pixels = new int[160 * 120] ;
   B.getPixels(color_pixels, 0, 160, 0, 0, 160, 120) ;
   B = null ; // free some memory

   // Convert the RGB color pixels to gray-scale
   return Image.gray_scale(color_pixels, 160, 120) ;
}

// This method applies an appropriate algorithm to compute the input
// image's gist vector.
private float[]
compute_gist_vector(Image I) throws GistEstimator.UnknownEstimator
{
   Utils.notify_main_thread(m_status_handler, STATUS_UPDATE_MSG,
                            R.string.computing_gist) ;

   GistEstimator GE = GistEstimator.create(Settings.gist_estimator()) ;
   float[] gist_vector = GE.apply(I) ;
   if (gist_vector == null) // something went horribly wrong!
      throw new RuntimeException("gist estimator bombed out") ;
   return gist_vector ;
}

// This method sends the gist vector to the server for scene
// classification.
private String send_to_server(float[] gist_vector)
{
   Utils.notify_main_thread(m_status_handler, STATUS_UPDATE_MSG,
                            R.string.sending_gist_to_server) ;

   String server = Settings.server_address() ;
   //*
   HttpFileUploader upload =
      new HttpFileUploader("http://" + server + "/upload_gv.php", "tmp.gv") ;
   upload.doStart(Utils.array_to_input_stream(gist_vector)) ;
   return "http://" + server + "/" + upload.getResultURL() ;
   //*/
   //return "http://www.google.com/" ;//TEMP HACK TO WORK AROUND ABSENT SERVER
}

// This method sends the entire image to the server instead of just the
// gist vector. The server will take care of computing the gist vector
// and performing the scene classification. When it's done, the server
// should return a URL for the result.
private String send_to_server(InputStream is)
{
   Utils.notify_main_thread(m_status_handler, STATUS_UPDATE_MSG,
                            R.string.sending_image_to_server) ;

   String server = Settings.server_address() ;
   //*
   HttpFileUploader htfu =
      new HttpFileUploader("http://"+server+"/upload.php", "tmp.jpg");//FIXXX
   htfu.doStart(is);

   return "http://" + server + "/" + htfu.getResultURL();
   //*/
   
}

} // end of inner class GistActivity.GistThread

//-----------------------------------------------------------------------

} 
